// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Immutable;
using System.Linq;
using ILLink.Shared;
using ILLink.Shared.TrimAnalysis;
using ILLink.Shared.TypeSystemProxy;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using MultiValue = ILLink.Shared.DataFlow.ValueSet<ILLink.Shared.DataFlow.SingleValue>;

namespace ILLink.RoslynAnalyzer
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public sealed class RequiresDynamicCodeAnalyzer : RequiresAnalyzerBase
    {
        private const string RequiresDynamicCodeAttribute = nameof(RequiresDynamicCodeAttribute);
        public const string FullyQualifiedRequiresDynamicCodeAttribute = "System.Diagnostics.CodeAnalysis." + RequiresDynamicCodeAttribute;

        private static readonly DiagnosticDescriptor s_requiresDynamicCodeOnStaticCtor = DiagnosticDescriptors.GetDiagnosticDescriptor(DiagnosticId.RequiresDynamicCodeOnStaticConstructor);
        private static readonly DiagnosticDescriptor s_requiresDynamicCodeOnEntryPoint = DiagnosticDescriptors.GetDiagnosticDescriptor(DiagnosticId.RequiresDynamicCodeOnEntryPoint);
        private static readonly DiagnosticDescriptor s_requiresDynamicCodeRule = DiagnosticDescriptors.GetDiagnosticDescriptor(DiagnosticId.RequiresDynamicCode);
        private static readonly DiagnosticDescriptor s_requiresDynamicCodeAttributeMismatch = DiagnosticDescriptors.GetDiagnosticDescriptor(DiagnosticId.RequiresDynamicCodeAttributeMismatch);
        private static readonly DiagnosticDescriptor s_referenceNotMarkedIsAotCompatibleRule = DiagnosticDescriptors.GetDiagnosticDescriptor(DiagnosticId.ReferenceNotMarkedIsAotCompatible);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
            ImmutableArray.Create(s_requiresDynamicCodeRule, s_requiresDynamicCodeAttributeMismatch, s_requiresDynamicCodeOnStaticCtor, s_requiresDynamicCodeOnEntryPoint, s_referenceNotMarkedIsAotCompatibleRule);

        private protected override string RequiresAttributeName => RequiresDynamicCodeAttribute;

        internal override string RequiresAttributeFullyQualifiedName => FullyQualifiedRequiresDynamicCodeAttribute;

        private protected override DiagnosticTargets AnalyzerDiagnosticTargets => DiagnosticTargets.MethodOrConstructor | DiagnosticTargets.Class;

        private protected override DiagnosticDescriptor RequiresDiagnosticRule => s_requiresDynamicCodeRule;

        private protected override DiagnosticId RequiresDiagnosticId => DiagnosticId.RequiresDynamicCode;

        private protected override DiagnosticDescriptor RequiresAttributeMismatch => s_requiresDynamicCodeAttributeMismatch;

        private protected override DiagnosticDescriptor RequiresOnStaticCtor => s_requiresDynamicCodeOnStaticCtor;

        private protected override DiagnosticDescriptor RequiresOnEntryPoint => s_requiresDynamicCodeOnEntryPoint;

        internal override bool IsAnalyzerEnabled(AnalyzerOptions options) =>
            options.IsMSBuildPropertyValueTrue(MSBuildPropertyOptionNames.EnableAotAnalyzer);

        internal override bool IsIntrinsicallyHandled(IMethodSymbol calledMethod, MultiValue instance, ImmutableArray<MultiValue> arguments)
        {
            MethodProxy method = new(calledMethod);
            var intrinsicId = Intrinsics.GetIntrinsicIdForMethod(method);

            switch (intrinsicId)
            {
                case IntrinsicId.Type_MakeGenericType:
                {
                    if (!instance.IsEmpty())
                    {
                        foreach (var value in instance.AsEnumerable())
                        {
                            if (value is SystemTypeValue typeValue)
                            {
                                if (!IsKnownInstantiation(arguments[0])
                                    && !IsConstrainedToBeReferenceTypes(typeValue.RepresentedType.GetGenericParameters()))
                                {
                                    return false;
                                }
                            }
                            else
                            {
                                return false;
                            }
                        }
                    }
                    return true;
                }
                case IntrinsicId.MethodInfo_MakeGenericMethod:
                {
                    if (!instance.IsEmpty())
                    {
                        foreach (var methodValue in instance.AsEnumerable())
                        {
                            if (methodValue is SystemReflectionMethodBaseValue methodBaseValue)
                            {
                                if (!IsKnownInstantiation(arguments[0])
                                    && !IsConstrainedToBeReferenceTypes(methodBaseValue.RepresentedMethod.GetGenericParameters()))
                                {
                                    return false;
                                }
                            }
                            else
                            {
                                return false;
                            }
                        }
                    }
                    return true;
                }
            }

            return false;

            static bool IsKnownInstantiation(MultiValue genericParametersArray)
            {
                var typesValue = genericParametersArray.AsSingleValue();
                if (typesValue is NullValue)
                {
                    // This will fail at runtime but no warning needed
                    return true;
                }

                // Is this an array we model?
                if (typesValue is not ArrayValue array)
                {
                    return false;
                }

                int? size = array.Size.AsConstInt();
                if (size == null)
                {
                    return false;
                }

                for (int i = 0; i < size.Value; i++)
                {
                    // Go over each element of the array. If the value is unknown, bail.
                    if (!array.TryGetValueByIndex(i, out MultiValue value))
                    {
                        return false;
                    }

                    var singleValue = value.AsSingleValue();

                    if (singleValue is not SystemTypeValue and not GenericParameterValue and not NullableSystemTypeValue)
                    {
                        return false;
                    }
                }

                return true;
            }

            static bool IsConstrainedToBeReferenceTypes(ImmutableArray<GenericParameterProxy> parameters)
            {
                foreach (GenericParameterProxy param in parameters)
                    if (!param.TypeParameterSymbol.HasReferenceTypeConstraint)
                        return false;
                return true;
            }
        }

        private protected override bool IsRequiresCheck(IPropertySymbol propertySymbol, Compilation compilation)
        {
            var runtimeFeaturesType = compilation.GetTypeByMetadataName("System.Runtime.CompilerServices.RuntimeFeature");
            if (runtimeFeaturesType == null)
                return false;

            var isDynamicCodeSupportedProperty = runtimeFeaturesType.GetMembers("IsDynamicCodeSupported").OfType<IPropertySymbol>().FirstOrDefault();
            if (isDynamicCodeSupportedProperty == null)
                return false;

            return SymbolEqualityComparer.Default.Equals(propertySymbol, isDynamicCodeSupportedProperty);
        }

        private protected override ImmutableArray<Action<CompilationAnalysisContext>> ExtraCompilationActions =>
            ImmutableArray.Create<Action<CompilationAnalysisContext>>((context) =>
            {
                CheckReferencedAssemblies(
                    context,
                    MSBuildPropertyOptionNames.VerifyReferenceAotCompatibility,
                    "IsAotCompatible",
                    s_referenceNotMarkedIsAotCompatibleRule);
            });

        protected override bool VerifyAttributeArguments(AttributeData attribute) =>
            attribute.ConstructorArguments.Length >= 1 && attribute.ConstructorArguments is [{ Type.SpecialType: SpecialType.System_String }, ..];

        protected override string GetMessageFromAttribute(AttributeData? requiresAttribute)
        {
            var message = (string)requiresAttribute!.ConstructorArguments[0].Value!;
            return MessageFormat.FormatRequiresAttributeMessageArg(message);
        }
    }
}
